package com.riiablo.excel;

import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;
import javax.annotation.Generated;
import javax.lang.model.element.Modifier;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateFormatUtils;

import com.badlogic.gdx.utils.Array;

import com.riiablo.excel.Excel.Entry.Column;
import com.riiablo.io.ByteInput;
import com.riiablo.io.ByteOutput;
import com.riiablo.logger.LogManager;
import com.riiablo.logger.Logger;

import static java.lang.reflect.Modifier.isTransient;

public class SerializerSourceGenerator {
  private static final Class<Serializer> SERIALIZER = Serializer.class;

  final Array<ColumnInfo> columns = new Array<>(true, 256, ColumnInfo.class);

  String sourcePackage;
  String serializerPackage;

  ClassName serializerName;
  ClassName excelName;
  ClassName entryName;

  public SerializerSourceGenerator() {
    this("com.riiablo.excel.txt", "com.riiablo.excel.serializer");
  }

  static final class ColumnInfo {
    final Column config;
    final Class type;
    final String name;

    ColumnInfo(Column config, Class type, String name) {
      this.config = config;
      this.type = type;
      this.name = name;
    }

    @Override
    public String toString() {
      return name;
    }
  }

  SerializerSourceGenerator(String sourcePackage, String serializerPackage) {
    this.sourcePackage = sourcePackage;
    this.serializerPackage = serializerPackage;
    serializerName = ClassName.get(SERIALIZER);
  }

  String serializerName() {
    return excelName.simpleName() + serializerName.simpleName();
  }

  String serializerSource() {
    return entryName.canonicalName();
  }

  SerializerSourceGenerator configure(Class excelClass, Class entryClass) {
    excelName = ClassName.get(excelClass);
    entryName = ClassName.get(entryClass);

    columns.clear();
    for (Field field : entryClass.getFields()) {
      if (isTransient(field.getModifiers())) continue;
      Column column = field.getAnnotation(Column.class);
      if (column == null) continue;
      columns.add(new ColumnInfo(
          column,
          field.getType(),
          field.getName()
          ));
    }

    return this;
  }

  JavaFile generateFile() {
    return generateFile(serializerSource());
  }

  JavaFile generateFile(String comment) {
    return generateFile(SerializerSourceGenerator.class, comment);
  }

  JavaFile generateFile(Class codeGenerator) {
    return generateFile(codeGenerator, serializerSource());
  }

  AnnotationSpec createGeneratedAnnotation(Class codeGenerator, String comment) {
    AnnotationSpec.Builder generated = AnnotationSpec
        .builder(Generated.class)
        .addMember("value", "$S", codeGenerator.getCanonicalName())
        .addMember("date", "$S", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(new Date()))
        ;
    if (StringUtils.isNotBlank(comment)) generated.addMember("comments", "$S", comment);
    return generated.build();
  }

  AnnotationSpec createSuppressWarningsAnnotation() {
    return AnnotationSpec
        .builder(SuppressWarnings.class)
        .addMember("value", "$S", "unused")
        .build();
  }

  JavaFile generateFile(Class codeGenerator, String comment) {
    Validate.notNull(excelName, "excelName not configured");
    Validate.notNull(entryName, "entryName not configured");
    TypeSpec.Builder serializerType = generate_Serializer()
        .addAnnotation(createGeneratedAnnotation(codeGenerator, comment))
        .addAnnotation(createSuppressWarningsAnnotation())
        ;
    return generateFile(serializerType.build());
  }

  JavaFile generateFile(TypeSpec serializerType) {
    return JavaFile
        .builder(serializerPackage, serializerType)
        .skipJavaLangImports(true)
        .addFileComment("automatically generated by $L, do not modify", SerializerSourceGenerator.class.getCanonicalName())
        .build();
  }

  TypeSpec.Builder generate_Serializer() {
    FieldSpec log = FieldSpec
        .builder(Logger.class, "log", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
        .initializer("$T.getLogger($N.class)", LogManager.class, serializerName())
        .build();
    return TypeSpec
        .classBuilder(serializerName())
        .addSuperinterface(ParameterizedTypeName.get(serializerName, entryName))
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addField(log)
        .addMethod(generate_readBin())
        .addMethod(generate_writeBin())
        .addMethod(generate_equals())
        .addMethod(generate_logErrors(log))
        ;
  }

  static CodeBlock qualify(Object object, Object field) {
    return CodeBlock.of("$N.$N", object, field);
  }

  static CodeBlock readX(Object in, Class type, Object var) {
    return CodeBlock.of("$L = $N.$N$L()", var, in, "read", getIoMethod(type));
  }

  static CodeBlock writeX(Object out, Class type, Object var) {
    return CodeBlock.of("$N.$N$L($L)", out, "write", getIoMethod(type), var);
  }

  static CodeBlock logX(Object log, Object message, Object args) {
    return CodeBlock.of("$N.$N($S, $L)", log, "error", message, args);
  }

  static CodeBlock equalsX(Type type, Object obj1, Object obj2) {
    return CodeBlock.of("$T.equals($L, $L)", type, obj1, obj2);
  }

  static CodeBlock defaultString(Object var) {
    return CodeBlock.of("$T.$N($L)", StringUtils.class, "defaultString", var);
  }

  static String getIoMethod(Type type) {
    if (type == String.class) {
      return "String";
    } else if (type == byte.class) {
      return "8";
    } else if (type == short.class) {
      return "16";
    } else if (type == int.class) {
      return "32";
    } else if (type == long.class) {
      return "64";
    } else if (type == boolean.class) {
      return "Boolean";
    } else {
      throw new UnsupportedOperationException(type + " is not supported!");
    }
  }

  MethodSpec generate_readBin() {
    ParameterSpec entry = ParameterSpec.builder(entryName, "entry").build();
    ParameterSpec in = ParameterSpec.builder(ByteInput.class, "in").build();
    MethodSpec.Builder method = MethodSpec
        .methodBuilder("readBin")
        .addAnnotation(Override.class)
        .addModifiers(Modifier.PUBLIC)
        .addParameter(entry)
        .addParameter(in)
        ;

    for (ColumnInfo column : columns) {
      final Column config = column.config;
      final Class type = column.type;
      final CodeBlock field = qualify(entry, column.name);
      if (type.isArray()) {
        final Class componentType = type.getComponentType();
        final String var = "x";
        method.addCode(CodeBlock.builder()
            .addStatement(
                "$L = new $T[$L]", field, componentType, config.endIndex() - config.startIndex())
            .beginControlFlow(
                "for (int $1N = $2L; $1N < $3L; $1N++)", var, 0, config.endIndex() - config.startIndex())
            .addStatement(
                readX(in, componentType, CodeBlock.of("$L[$N]", field, var)))
            .endControlFlow()
            .build());
      } else {
        method.addCode(CodeBlock.builder()
            .addStatement(readX(in, type, field))
            .build());
      }
    }

    return method.build();
  }

  MethodSpec generate_writeBin() {
    ParameterSpec entry = ParameterSpec.builder(entryName, "entry").build();
    ParameterSpec out = ParameterSpec.builder(ByteOutput.class, "out").build();
    MethodSpec.Builder method = MethodSpec
        .methodBuilder("writeBin")
        .addAnnotation(Override.class)
        .addModifiers(Modifier.PUBLIC)
        .addParameter(entry)
        .addParameter(out)
        ;

    for (ColumnInfo column : columns) {
      final Class type = column.type;
      final CodeBlock field = qualify(entry, column.name);
      if (type.isArray()) {
        final Class componentType = type.getComponentType();
        final String var = "x";
        method.addCode(CodeBlock.builder()
            .beginControlFlow(
                "for ($T $N : $L)", componentType, var, field)
            .addStatement(componentType == String.class
                ? writeX(out, componentType, defaultString(var))
                : writeX(out, componentType, var))
            .endControlFlow()
            .build());
      } else {
        method.addCode(CodeBlock.builder()
            .addStatement(type == String.class
                ? writeX(out, type, defaultString(field))
                : writeX(out, type, field))
            .build());
      }
    }

    return method.build();
  }

  MethodSpec generate_equals() {
    ParameterSpec e1 = ParameterSpec.builder(entryName, "e1").build();
    ParameterSpec e2 = ParameterSpec.builder(entryName, "e2").build();
    MethodSpec.Builder method = MethodSpec
        .methodBuilder("equals")
        .addAnnotation(Override.class)
        .addModifiers(Modifier.PUBLIC)
        .returns(boolean.class)
        .addParameter(e1)
        .addParameter(e2)
        ;

    for (ColumnInfo column : columns) {
      final Class type = column.type;
      final String name = column.name;
      final CodeBlock e1Field = qualify(e1, name);
      final CodeBlock e2Field = qualify(e2, name);
      final CodeBlock.Builder block = CodeBlock.builder();
      if (type.isPrimitive()) {
        block.beginControlFlow("if ($L != $L)",
            e1Field,
            e2Field);
      } else {
        block.beginControlFlow("if (!$L)",
            equalsX(
                type.isArray() ? Arrays.class : Objects.class,
                e1Field,
                e2Field));
      }

      method.addCode(block
          .addStatement("return false")
          .endControlFlow()
          .build());
    }

    method.addStatement("return true");
    return method.build();
  }

  MethodSpec generate_logErrors(FieldSpec log) {
    ParameterSpec e1 = ParameterSpec.builder(entryName, "e1").build();
    ParameterSpec e2 = ParameterSpec.builder(entryName, "e2").build();
    MethodSpec.Builder method = MethodSpec
        .methodBuilder("logErrors")
        .addAnnotation(Override.class)
        .addModifiers(Modifier.PUBLIC)
        .addParameter(e1)
        .addParameter(e2)
        ;

    for (ColumnInfo column : columns) {
      final Class type = column.type;
      final String name = column.name;
      final CodeBlock e1Field = qualify(e1, name);
      final CodeBlock e2Field = qualify(e2, name);
      final CodeBlock.Builder block = CodeBlock.builder();
      if (type.isPrimitive()) {
        block.beginControlFlow("if ($L != $L)",
            e1Field,
            e2Field);
      } else {
        block.beginControlFlow("if (!$L)",
            equalsX(
                type.isArray() ? Arrays.class : Objects.class,
                e1Field,
                e2Field));
      }

      method.addCode(block
          .addStatement(logX(log,
              CodeBlock.of("$L does not match: $N={}, $N={}", name, e1, e2),
              CodeBlock.of("$L, $L", e1Field, e2Field)))
          .endControlFlow()
          .build());
    }

    return method.build();
  }
}
